using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using XnaModel = Microsoft.Xna.Framework.Graphics.Model;

namespace SpaceWolf.Graphics
{
    /// <summary>
    /// Model class for loading and displaying x files including all materials
    /// and textures. Provides load and render functions to render static
    /// non animated models (mostly 1 mesh), for animated models we need a more
    /// advanced class, which is not required for this game yet.
    /// </summary>
    public class Model : IDisposable
    {
        #region Variables
        /// <summary>
        /// Name of this model, also used to load it from the content system.
        /// </summary>
        string name = "";

        /// <summary>
        /// Underlying xna model object. Loaded with the content system.
        /// </summary>
        XnaModel xnaModel = null;

        /*not longer required
        /// <summary>
        /// Scaling factor from 3ds max to our engine (1 unit = 1 meter)
        /// </summary>
        const float MaxModelScaling = 1.0f;
         */

        /// <summary>
        /// Default object matrix to fix models from 3ds max to our engine!
        /// </summary>
        static readonly Matrix objectMatrix =
            //right handed models: Matrix.CreateRotationX(MathHelper.Pi);// *
            //Matrix.CreateScale(MaxModelScaling);
            // left handed models (else everything is mirrored with x files)
            Matrix.CreateRotationX(MathHelper.Pi / 2.0f);

        /// <summary>
        /// Transform matrices for this model, used in all Render methods,
        /// build once in the constructor, it never changes and none of our
        /// models are animated.
        /// </summary>
        Matrix[] transforms = null;
        /// <summary>
        /// Scaling for this object, used for distance comparisons.
        /// </summary>
        float realScaling = 1.0f, scaling = 1.0f;

        /// <summary>
        /// Does this model has alpha textures? Then render with alpha blending
        /// turned on. This is usually false and rendering is faster without
        /// alpha blending. Also used to skip shadow receiving, which looks
        /// strange on palms.
        /// </summary>
        bool hasAlpha = false;

        /// <summary>
        /// Is this the car model? Set in constructor and used in the render
        /// methods, this way we can compare much faster when rendering!
        /// </summary>
        //bool isCar = false;

        /// <summary>
        /// If we want to animated some mesh in the model, just set the
        /// modelmesh here. Used for the windmill, which is rotated in
        /// Render!
        /// </summary>
        ModelMesh animatedMesh = null;

        /// <summary>
        /// Cached effect parameters to improve performance.
        /// For around 100 000 objects we save 1 second per effect parameters
        /// call. We only save the world matrix, worldViewProj matrix,
        /// viewInverse matrix and the lightDir vector effect parameters.
        /// Update: We also save diffuseTexture, ambientColor and diffuseColor now
        /// </summary>
        List<EffectParameter> cachedEffectParameters =
            new List<EffectParameter>();

        /// <summary>
        /// Another helper to check if the effect technique is
        /// "ReflectionSpecular". Checking this each frame takes a lot of time,
        /// this helper does the check only once in the constuctor.
        /// Used in RenderCar!
        /// </summary>
        List<bool> cachedIsReflectionSpecularTechnique =
            new List<bool>();

        /// <summary>
        /// Renderable meshes dictionary. Used to render every RenderableMesh
        /// in our render method.
        /// </summary>
        Dictionary<ModelMeshPart, MeshRenderManager.RenderableMesh>
            renderableMeshes =
            new Dictionary<ModelMeshPart, MeshRenderManager.RenderableMesh>();
        #endregion

        #region Properties
        /// <summary>
        /// Name for this model, this is the content name.
        /// </summary>
        /// <returns>String</returns>
        public string Name
        {
            get
            {
                return name;
            } // get
        } // Name

        /// <summary>
        /// Size
        /// </summary>
        /// <returns>Float</returns>
        public float Size
        {
            get
            {
                return realScaling;
            } // get
        } // Size

        /// <summary>
        /// Number of mesh parts
        /// </summary>
        /// <returns>Int</returns>
        public int NumOfMeshParts
        {
            get
            {
                int ret = 0;
                for (int meshNum = 0; meshNum < xnaModel.Meshes.Count; meshNum++)
                    ret += xnaModel.Meshes[meshNum].MeshParts.Count;
                return ret;
            } // get
        } // NumOfMeshParts
        #endregion

        #region Constructor
        /// <summary>
        /// Create model
        /// </summary>
        /// <param name="setModelName">Set model name</param>
        public Model(string setModelName)
        {
            name = setModelName;

            xnaModel = SpaceWolf.BaseGame.ContentManager.Load<XnaModel>(
                name);

            // Get matrix transformations of the model
            // Has to be done only once because we don't use animations in our game.
            if (xnaModel != null)
            {
                transforms = new Matrix[xnaModel.Bones.Count];
                xnaModel.CopyAbsoluteBoneTransformsTo(transforms);

                // Calculate scaling for this object, used for distance comparisons.
                if (xnaModel.Meshes.Count > 0)
                    realScaling = scaling =
                        xnaModel.Meshes[0].BoundingSphere.Radius *
                        transforms[0].Right.Length();

                // Don't use more than 3m for scaling and checking smaller objects
                if (scaling > 3)
                    scaling = 3;
            } // if (xnaModel)

            hasAlpha = name.StartsWith("Alpha");
            // Is this a sign or banner? Then make sure ambient is pretty high!
            //bool isSign = name.StartsWith("Sign") ||
            //    name.StartsWith("Banner") ||
            //    // Also include windmills, they are too dark
            //    name.StartsWith("Windmill");
            //name.StartsWith("StartLight");

            //isCar = name == "Car";

            // Go through all meshes in the model
            for (int meshNum = 0; meshNum < xnaModel.Meshes.Count; meshNum++)
            {
                ModelMesh mesh = xnaModel.Meshes[meshNum];
                int meshPartNum = 0;
                string meshName = mesh.Name;

                // Remember this mesh for animations done in Render!
                //if (name == "Windmill" &&
                //    meshName.StartsWith("Windmill_Wings"))
                //    animatedMesh = mesh;

                // And for each effect this mesh uses (usually just 1, multimaterials
                // are nice in 3ds max, but not efficiently for rendering stuff).
                for (int effectNum = 0; effectNum < mesh.Effects.Count; effectNum++)
                {
                    Effect effect = mesh.Effects[effectNum];
                    // Store our 4 effect parameters
                    cachedEffectParameters.Add(effect.Parameters["diffuseTexture"]);
                    cachedEffectParameters.Add(effect.Parameters["ambientColor"]);
                    cachedEffectParameters.Add(effect.Parameters["diffuseColor"]);
                    cachedEffectParameters.Add(effect.Parameters["world"]);
                    cachedEffectParameters.Add(effect.Parameters["viewProj"]);
                    cachedEffectParameters.Add(effect.Parameters["viewInverse"]);
                    cachedEffectParameters.Add(effect.Parameters["lightDir"]);


                    // Store if this is a "ReflectionSpecular" technique
                    cachedIsReflectionSpecularTechnique.Add(
                        effect.CurrentTechnique.Name.Contains("ReflectionSpecular"));

                    // Increase ambient value to 0.5, 0.5, 0.5 for signs and banners!
                    //if (isSign)
                    //    effect.Parameters["ambientColor"].SetValue(
                    //        new Color(128, 128, 128).ToVector4());

                    // Get technique from meshName
                    int techniqueIndex = -1;
                    if (meshName.Length > meshPartNum)
                    {
                        string techniqueNumberString = meshName.Substring(
                            meshName.Length - (1 + meshPartNum), 1);

                        try
                        {
                            techniqueIndex = Convert.ToInt32(techniqueNumberString);
                        } // try
                        catch { } // ignore if that failed

                    } // if (meshName.Length)

                    // No technique found or invalid?
                    if (techniqueIndex < 0 ||
                        techniqueIndex >= effect.Techniques.Count)
                    {
                        // Try to use last technique
                        techniqueIndex = effect.Techniques.Count - 1;
                        // If this is NormalMapping, use DiffuseSpecular20 instead
                        // of the last technique (which is SpecularWithReflection20)
                        if (effect.Techniques[techniqueIndex].Name.Contains(
                            "SpecularWithReflection"))
                            techniqueIndex -= 2;
                        // Update: We have now 2 more techniques (ReflectionSpecular)
                        if (effect.Techniques[techniqueIndex].Name.Contains(
                            "ReflectionSpecular"))
                            techniqueIndex -= 4;
                    } // if (techniqueIndex)

                    // If the technique ends with 20, but we can't do ps20,
                    // use the technique before that (which doesn't use ps20)
                    //if (BaseGame.CanUsePS20 == false &&
                    //    effect.Techniques[techniqueIndex].Name.EndsWith("20"))
                    //techniqueIndex--;

                    // Set current technique for rendering below
                    effect.CurrentTechnique = effect.Techniques[techniqueIndex];

                    // Next mesh part
                    meshPartNum++;
                } // foreach (effect)

                // Add all mesh parts!
                for (int partNum = 0; partNum < mesh.MeshParts.Count; partNum++)
                {
                    ModelMeshPart part = mesh.MeshParts[partNum];
                    // The model mesh part is not really used, we just extract the
                    // index and vertex buffers and all the render data.
                    // Material settings are build from the effect settings.
                    // Also add this to our own dictionary for rendering.
                    renderableMeshes.Add(part, SpaceWolf.BaseGame.MeshRenderManager.Add(
                        mesh.VertexBuffer, mesh.IndexBuffer, part, part.Effect));
                } // for (partNum)
            } // foreach (mesh)
        } // Model(setModelName)
        #endregion

        #region Dispose
        /// <summary>
        /// Dispose
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        } // Dispose()

        /// <summary>
        /// Dispose
        /// </summary>
        /// <param name="disposing">Disposing</param>
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                // Just set everything to null so we stop using this!
                name = "";
                xnaModel = null;
                transforms = null;
                animatedMesh = null;
            } // if
        } // Dispose(disposing)
        #endregion

        #region Render
        /// <summary>
        /// Default view distance optimizer is at 250m, then skip stuff.
        /// This will be reduced as our framerate runs low to improve performance
        /// on low end systems.
        /// </summary>
        static int maxViewDistance = 200;//250;

        /// <summary>
        /// Maximum view distance
        /// </summary>
        /// <returns>Int</returns>
        public static int MaxViewDistance
        {
            get
            {
                return maxViewDistance;
            } // get
            set
            {
                // Only set if we reduce, don't increase again if it is running
                // a little faster for a short time.
                if (value < maxViewDistance)
                    maxViewDistance = value;
            } // set
        } // MaxViewDistance

        /// <summary>
        /// Render
        /// </summary>
        /// <param name="renderMatrix">Render matrix</param>
        public void Render(Matrix renderMatrix)
        {
            // Optimization to skip smaller objects, which are very far away!
            // Display 1 meter big objects only if in a distance of 250 meters!
            // Scaling is guessed by the length of the first vector in our matrix,
            // because we always use the same scaling for x, y, z this should be
            // correct!
            float maxDistance = maxViewDistance * scaling;
            float distanceSquared = Vector3.DistanceSquared(
                SpaceWolf.BaseGame.CameraPos, renderMatrix.Translation);
            if (distanceSquared > maxDistance * maxDistance)
                // Don't render, too far away!
                return;

            // Check out if object is behind us or not visible, then we can skip
            // rendering. This is the GREATEST performance gain in the whole game!
            // Object must be away at least 20 units!
            if (distanceSquared > 20 * 20 &&
                // And the object size must be small
                distanceSquared > (10 * scaling) * (10 * scaling))
            {
                Vector3 objectDirection =
                    Vector3.Normalize(SpaceWolf.BaseGame.CameraPos - renderMatrix.Translation);

                // Half field of view should be fov / 2, but because of
                // the aspect ratio (1.33) and an additional offset we need
                // to include to see objects at the borders.
                float objAngle = Vector3Helper.GetAngleBetweenVectors(
                    SpaceWolf.BaseGame.CameraRotation, objectDirection);
                if (objAngle > SpaceWolf.BaseGame.ViewableFieldOfView)
                    // Skip.
                    return;
            } // if (distanceSquared)

            // Multiply object matrix by render matrix, result is used multiple
            // times here.
            renderMatrix = objectMatrix * renderMatrix;

            // Go through all meshes in the model
            for (int meshNum = 0; meshNum < xnaModel.Meshes.Count; meshNum++)
            {
                ModelMesh mesh = xnaModel.Meshes[meshNum];

                // Assign world matrix
                Matrix worldMatrix =
                    transforms[mesh.ParentBone.Index] *
                    renderMatrix;

                // Got animation?
                if (animatedMesh == mesh)
                {
                    worldMatrix =
                        Matrix.CreateRotationZ(
                        // Use pseudo number for this object for different rotations
                        renderMatrix.Translation.Length() * 3 +
                        renderMatrix.Determinant() * 5 +
                        (1.0f + ((int)(renderMatrix.M42 * 33.3f) % 100) * 0.00123f) *
                        SpaceWolf.BaseGame.TotalTime / 0.654f) *
                        transforms[mesh.ParentBone.Index] *
                        renderMatrix;
                } // if (animatedMesh)

                // Just add this world matrix to our render matrices for each part.
                for (int partNum = 0; partNum < mesh.MeshParts.Count; partNum++)
                {
                    // Find mesh part in the renderableMeshes dictionary and add the
                    // new render matrix to be picked up in the mesh rendering later.
                    renderableMeshes[mesh.MeshParts[partNum]].renderMatrices.Add(
                        worldMatrix);
                } // for (partNum)
            } // foreach (mesh)
        } // Render(renderMatrix)

        /// <summary>
        /// Render
        /// </summary>
        /// <param name="renderMatrix">Render matrix</param>
        public void Render(Color meshColor, Matrix renderMatrix)
        {
            // Optimization to skip smaller objects, which are very far away!
            // Display 1 meter big objects only if in a distance of 250 meters!
            // Scaling is guessed by the length of the first vector in our matrix,
            // because we always use the same scaling for x, y, z this should be
            // correct!
            float maxDistance = maxViewDistance * scaling;
            float distanceSquared = Vector3.DistanceSquared(
                SpaceWolf.BaseGame.CameraPos, renderMatrix.Translation);
            if (distanceSquared > maxDistance * maxDistance)
                // Don't render, too far away!
                return;

            // Check out if object is behind us or not visible, then we can skip
            // rendering. This is the GREATEST performance gain in the whole game!
            // Object must be away at least 20 units!
            if (distanceSquared > 20 * 20 &&
                // And the object size must be small
                distanceSquared > (10 * scaling) * (10 * scaling))
            {
                Vector3 objectDirection =
                    Vector3.Normalize(SpaceWolf.BaseGame.CameraPos - renderMatrix.Translation);

                // Half field of view should be fov / 2, but because of
                // the aspect ratio (1.33) and an additional offset we need
                // to include to see objects at the borders.
                float objAngle = Vector3Helper.GetAngleBetweenVectors(
                    SpaceWolf.BaseGame.CameraRotation, objectDirection);
                if (objAngle > SpaceWolf.BaseGame.ViewableFieldOfView)
                    // Skip.
                    return;
            } // if (distanceSquared)

            for (int meshNum = 0; meshNum < xnaModel.Meshes.Count; meshNum++)
            {
                ModelMesh mesh = xnaModel.Meshes[meshNum];

                for (int effectNum = 0; effectNum < mesh.Effects.Count; effectNum++)
                {
                    Effect effect = mesh.Effects[effectNum];
                    //if (effectNum == 0)
                    //    remCurrentTechnique = effect.CurrentTechnique;


                    effect.CurrentTechnique =
                               effect.Techniques["Diffuse20"];

                    renderMatrix = objectMatrix * renderMatrix;
                    // Assign world matrix
                    BaseGame.WorldMatrix =transforms[mesh.ParentBone.Index]* renderMatrix;

                    cachedEffectParameters[3].SetValue(
                            BaseGame.WorldMatrix);
                    // Set matrices
                    effect.Parameters["diffuseColor"].SetValue(
                            meshColor.ToVector4());
                    effect.Parameters["worldViewProj"].SetValue(
                        BaseGame.WorldViewProjectionMatrix);
                    effect.Parameters["center_to_viewer"].SetValue(
                        BaseGame.WorldMatrix.Forward);
                    
                    // Render
                }
                mesh.Draw();
             
                //if (meshColor != Color.Black &&
                //    remCurrentTechnique != null)
                //{
                //    mesh.Effects[0].CurrentTechnique = remCurrentTechnique;
                //}
            }
        } // Render(renderMatrix)

        /// <summary>
        /// Render
        /// </summary>
        /// <param name="renderPos">Render position</param>
        public void Render(Vector3 renderPos)
        {
            Render(Matrix.CreateTranslation(renderPos));
        } // Render(renderPos)
        #endregion
    } // class Model
}
